package com.xw.unification.entity;

import com.xw.unification.annotations.ApiVersion;
import com.xw.unification.enums.ApiVersionConditionType;
import com.xw.unification.handler.PathVersionHandlerMapping;
import com.xw.unification.enums.VersionMatchStrategy;
import com.xw.unification.config.WebUnificationMvcProperties;
import com.xw.unification.helper.ApiVersionHelper;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * API 版本条件定义类
 *
 * @author qiwei
 * @date 2023-8-9 21:00:00
 * @since v1.0.0
 */
@Getter
@Setter
public final class ApiVersionCondition extends ApiVersionHelper implements RequestCondition<ApiVersionCondition> {

    public static final Logger LOGGER = LoggerFactory.getLogger(ApiVersionCondition.class);

    private static final char VERSION_PATH_KEY_START_PREFIX = '{';
    private static final char VERSION_PATH_KEY_END_SUFFIX = '}';
    private static final char VERSION_PATH_KEY_ESCAPE = '\\';
    private static final String VERSION_PATH_KEY_PREFIX = "v";
    private static final String VERSION_PATH_SPLITTER = "/+";
    private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile(VERSION_PATH_KEY_PREFIX + "(\\d+(?:\\.\\d+)*)");
    private static final ApiVersionDefaultComparator VERSION_COMPARATOR = new ApiVersionDefaultComparator();
    private final PathVersionHandlerMapping handler;
    private WebUnificationMvcProperties.ApiVersionProperties properties;
    private ApiVersionConditionType conditionType;
    private PathMatcher pathMatcher;
    private ApiVersion apiVersionAnno;
    private Class<?> rawType;
    private Method method;
    private int level;
    private String versionPath;
    private String versionStr;

    public ApiVersionCondition(PathVersionHandlerMapping handler, Class<?> rawType, ApiVersion apiVersion,
                               WebUnificationMvcProperties.ApiVersionProperties properties) {
        if (apiVersion == null || rawType == null) {
            throw new IllegalArgumentException("Api version and class type can not be null");
        }
        if (isBlankString(apiVersion.key())) {
            throw new IllegalArgumentException("Key can not be blank");
        }
        this.handler = handler;
        this.rawType = rawType;
        this.conditionType = ApiVersionConditionType.TYPE;
        this.apiVersionAnno = apiVersion;
        this.properties = properties;
    }

    public ApiVersionCondition(PathVersionHandlerMapping handler, Method method, ApiVersion apiVersion,
                               WebUnificationMvcProperties.ApiVersionProperties properties) {
        if (apiVersion == null || method == null) {
            throw new IllegalArgumentException("Api version and method can not be null");
        }
        if (isBlankString(apiVersion.key())) {
            throw new IllegalArgumentException("Key can not be blank");
        }
        this.handler = handler;
        this.method = method;
        this.conditionType = ApiVersionConditionType.METHOD;
        this.apiVersionAnno = apiVersion;
        this.properties = properties;
    }
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        // 采用最后定义优先原则，则方法上的定义覆盖类上面的定义
        setApiVersionAnno(other.getApiVersionAnno());
        handler.removeCondition(other);
        return this;
    }
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
        StringBuilder builder = new StringBuilder();
        builder.append(VERSION_PATH_KEY_START_PREFIX);
        builder.append(apiVersionAnno.key());
        builder.append(VERSION_PATH_KEY_END_SUFFIX);
        String replaceStr = builder.toString();
        builder.insert(0, VERSION_PATH_KEY_ESCAPE);
        builder.insert(builder.length() - 1, VERSION_PATH_KEY_ESCAPE);
        String replacePattern = builder.toString();
        level = 0;
        versionPath = null;
        versionStr = null;
        if (conditionType == ApiVersionConditionType.METHOD && method != null) {
            ControllerMapping controllerMapping = ControllerMappingUtil.mappingMethod(method);
            return matchPattern(controllerMapping, httpServletRequest, replacePattern, replaceStr) ? this : null;
        } else if (conditionType == ApiVersionConditionType.TYPE && rawType != null) {
            Map<Method, ControllerMapping> mappingMap = ControllerMappingUtil.mappingClass(rawType);
            if (mappingMap.isEmpty()) {
                return null;
            }
            ControllerMapping controllerMapping;
            boolean match;
            for (Method method : mappingMap.keySet()) {
                controllerMapping = mappingMap.get(method);
                match = matchPattern(controllerMapping, httpServletRequest, replacePattern, replaceStr);
                if (match) {
                    return this;
                }
            }
            return null;
        } else {
            return null;
        }
    }

    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        if (properties.getVersionMatchStrategy() != VersionMatchStrategy.STRICT_EQUALS) {
            int compare = getLevel() - other.getLevel();
            if (compare != 0) {
                if (compare > 0) {
                    setVersion(other.versionStr);
                } else {
                    setVersion(this.versionStr);
                }
                return compare;
            } else {
                // 此处使用含精度的比较
                compare = VERSION_COMPARATOR.compareOnlyDigit(
                        getApiVersionAnno().value(), other.getApiVersionAnno().value(), false);
                if (compare > 0) {
                    setVersion(other.versionStr);
                } else {
                    setVersion(this.versionStr);
                }
                if (properties.getVersionMatchStrategy() == VersionMatchStrategy.MAX_IN_LOW) {
                    // 取最大的
                    if (compare > 0) {
                        setVersion(this.versionStr);
                    } else {
                        setVersion(other.versionStr);
                    }
                    return -compare;
                } else if (properties.getVersionMatchStrategy() == VersionMatchStrategy.MIN_IN_HIGH) {
                    // 取最小的
                    return compare;
                } else {
                    return compare;
                }
            }
        } else {
            return 0;
        }
    }

    public void setApiVersionAnno(ApiVersion apiVersionAnno) {
        this.apiVersionAnno = apiVersionAnno;
    }

    private boolean isBlankString(String string) {
        return string == null || "".equals(string.trim());
    }

    private boolean matchPattern(ControllerMapping controllerMapping, HttpServletRequest request, String replacePattern, String replaceStr) {
        String requestURI = request.getRequestURI();
        VersionMatchStrategy matchStrategy = properties.getVersionMatchStrategy();
        RequestMappingInfo requestMappingInfo = controllerMapping.getRequestMappingInfo();
        List<String> matchingPatterns = getMatchPatterns(requestMappingInfo, request, requestURI);
        if (matchingPatterns == null || matchingPatterns.isEmpty()) {
            return false;
        }
        boolean match = false;
        versionStr = apiVersionAnno.value();
        String pathVersionStr = null;
        for (String matchingPattern : matchingPatterns) {
            pathVersionStr = matchingPattern.replaceFirst(replacePattern, VERSION_PATH_KEY_PREFIX + versionStr);
            if (pathVersionStr.equals(matchingPattern)) {
                // 没有替换, 不匹配 {anno.key()}, 继续查找
                continue;
            }
            if (matchStrategy != null && matchStrategy != VersionMatchStrategy.STRICT_EQUALS) {
                try {
                    // 请求路径上的版本号
                    pathVersionStr = getPathVersionStr(requestURI, matchingPattern, replaceStr);
                    if (pathVersionStr == null || !VERSION_PREFIX_PATTERN.matcher(pathVersionStr).matches()) {
                        continue;
                    }
                    versionPath = pathVersionStr;
                    // 比较 API 版本号 和 请求路径上的版本号, 使用不含精度的比较
                    int compare = VERSION_COMPARATOR.compareOnlyDigit(apiVersionAnno.value(), pathVersionStr, true);
                    if (compare == 0) {
                        level = -1;
                    }
                    if (matchStrategy == VersionMatchStrategy.MAX_IN_LOW && compare <= 0) {
                        match = true;
                        break;
                    } else if (matchStrategy == VersionMatchStrategy.MIN_IN_HIGH && compare >= 0) {
                        match = true;
                        break;
                    } else {
                        if (pathVersionStr.equals(VERSION_PATH_KEY_PREFIX + apiVersionAnno.value())) {
                            level = -2;
                        }
                        if (compare == 0) {
                            match = true;
                            break;
                        }
                    }
                } catch (Exception e) {
                    // ignore the exception
                    continue;
                }
            } else {
                match = requestURI.equals(pathVersionStr);
                if (match) {
                    break;
                }
            }
        }
        if (match) {
            ApiVersionCondition.setVersion(versionStr);
        }
        return match;
    }

    private String getPathVersionStr(String requestURI, String matchingPattern, String replaceStr) {
        if (replaceStr == null) {
            return null;
        }
        String[] uriList = requestURI.split(VERSION_PATH_SPLITTER);
        String[] patternList = matchingPattern.split(VERSION_PATH_SPLITTER);
        if (uriList.length != patternList.length) {
            return null;
        }
        int index = -1;
        for (int i = 0; i < patternList.length; i++) {
            if (replaceStr.equals(patternList[i])) {
                index = i;
                break;
            }
        }
        return index < 0 ? null : uriList[index];
    }

    private List<String> getMatchPatterns(RequestMappingInfo mappingInfo, HttpServletRequest request, String requestURI) {
        PatternsRequestCondition patternsCondition = mappingInfo.getPatternsCondition();
        if (patternsCondition != null) {
            PatternsRequestCondition condition = patternsCondition.getMatchingCondition(request);
            return condition == null ? Collections.emptyList() : condition.getMatchingPatterns(requestURI);
        }
        try {
            PathPatternsRequestCondition pathPatternsCondition = mappingInfo.getPathPatternsCondition();
            if (pathPatternsCondition != null) {
                PathPatternsRequestCondition condition = pathPatternsCondition.getMatchingCondition(request);
                if (condition == null) {
                    return Collections.emptyList();
                }
                Set<String> patternValues = condition.getPatternValues();
                return patternValues.isEmpty() ? Collections.emptyList() : new ArrayList<>(patternValues);
            }
        } catch (Throwable e) {
            LOGGER.error("Failed to resolve match patterns of PathPatternsRequestCondition", e);
        }
        return Collections.emptyList();
    }

    protected static void setVersion(String version) {
        ApiVersionHelper.setVersion(version);
    }

    protected static void removeVersion() {
        ApiVersionHelper.removeVersion();
    }

}
